﻿using System;
using System.Collections.Generic;
using System.Linq;
using SlamCms.Common;

namespace SlamCms.Data
{
	// TODO Include Features (include a list of the features for each content item)

	public class QueryBuilder : IFluentInterface
	{
		private readonly DataContext _dataContext;
		private readonly QueryParameters _parameters;

		internal QueryBuilder(DataContext dataContext)
		{
			_dataContext = dataContext;
			_parameters = new QueryParameters(dataContext.Configuration.ContentTypes);
		}

		public DataContext Context
		{
			get
			{
				return _dataContext;
			}
		}

		public ContentTypeCollection ContentTypes
		{
			get
			{
				return _parameters.ContentTypes;
			}
		}

		public QueryParameters Parameters
		{
			get
			{
				return _parameters;
			}
		}

		public QueryBuilder Cache(QueryCacheBehavior cacheBehavior)
		{
			_parameters.Cache.Behavior = cacheBehavior;
			return this;
		}

		public QueryBuilder Cache(int duration)
		{
			_parameters.Cache.Duration = duration;
			return this;
		}

		/// <summary>
		/// Adds a name to the query used for caching purposes.
		/// </summary>
		/// <param name="name">The name you want for the query</param>
		/// <returns></returns>
		public QueryBuilder Named(string name)
		{
			_parameters.Name = name;
			return this;
		}

		/// <summary>
		/// Limits the number of items to retrieve that match criteria.
		/// </summary>
		/// <param name="take">The maximum number of content items to retrieve.</param>
		/// <returns></returns>
		public QueryBuilder Take(int take)
		{
			_parameters.Take = take;
			return this;
		}

		/// <summary>
		/// Each content item in the result will include its associated tags.
		/// </summary>
		/// <returns></returns>
		public QueryBuilder IncludeTags()
		{
			_parameters.IncludeTags = true;
			return this;
		}

		/// <summary>
		/// Adds a custom filter to the where clause using SQL.
		/// </summary>
		/// <param name="filter">The SQL where clause for the custom filter. i.e. .Filter("ContentItem.ModifiedDate &lt; '20120205'")</param>
		/// <param name="parameters">The parameters for the filter. i.e. .Filter("Url = '{0}'", url)</param>
		/// <returns></returns>
		public QueryBuilder Filter(string filter, params object[] parameters)
		{
			if (filter.IsNullOrEmpty())
				return this;

			_parameters.AdditionalFilters.Add(filter.F(parameters));
			return this;
		}

		/// <summary>
		/// Adds a filter by status to the where clause. Using FilterStatus. None will remove the status filter.
		/// </summary>
		/// <param name="filterStatus">The status you want to filter by</param>
		/// <returns></returns>
		public QueryBuilder FilterStatus(FilterStatus filterStatus)
		{
			_parameters.Status = filterStatus;
			return this;
		}

		/// <summary>
		/// Filters 
		/// </summary>
		/// <param name="contentItemIds"></param>
		/// <returns></returns>
		public QueryBuilder FilterContentItemId(params string[] contentItemIds)
		{
			if (contentItemIds == null)
				return this;

			var filteredContentItemIds = contentItemIds.Where(x => x != null);
			_parameters.ContentItemIds.AddRange(filteredContentItemIds);
			return this;
		}

		/// <summary>
		/// Adds a filter by type to the query. i.e. .FilterContentTypes&lt;Download&gt;() where Download is a content type.
		/// </summary>
		/// <typeparam name="T">The content type to filter</typeparam>
		/// <returns></returns>
		public QueryBuilder FilterContentTypes<T>() where T : ContentItem, new()
		{
			return FilterContentTypes(typeof(T));
		}

		/// <summary>
		/// Adds a filter by type to the query. i.e. .FilterContentTypes&lt;Download, Event&gt;() where Download and Event are content types.
		/// </summary>
		/// <typeparam name="T1">The content type to filter</typeparam>
		/// <typeparam name="T2">The content type to filter</typeparam>
		/// <returns></returns>
		public QueryBuilder FilterContentTypes<T1, T2>()
			where T1 : ContentItem, new()
			where T2 : ContentItem, new()
		{
			return FilterContentTypes(typeof(T1), typeof(T2));
		}

		/// <summary>
		/// Adds a filter by type to the query. i.e. .FilterContentTypes&lt;Download, Event, Faq&gt;() where Download, Event and Faq are content types.
		/// </summary>
		/// <typeparam name="T1">The content type to filter</typeparam>
		/// <typeparam name="T2">The content type to filter</typeparam>
		/// <typeparam name="T3">The content type to filter</typeparam>
		/// <returns></returns>
		public QueryBuilder FilterContentTypes<T1, T2, T3>()
			where T1 : ContentItem, new()
			where T2 : ContentItem, new()
			where T3 : ContentItem, new()
		{
			return FilterContentTypes(typeof(T1), typeof(T2), typeof(T3));
		}

		/// <summary>
		/// Adds a filter by type to the query. i.e. .FilterContentTypes&lt;Download, Event, Faq, Page&gt;() where Download, Event, Faq and Page are content types.
		/// </summary>
		/// <typeparam name="T1">The content type to filter</typeparam>
		/// <typeparam name="T2">The content type to filter</typeparam>
		/// <typeparam name="T3">The content type to filter</typeparam>
		/// <typeparam name="T4">The content type to filter</typeparam>
		/// <returns></returns>
		public QueryBuilder FilterContentTypes<T1, T2, T3, T4>()
			where T1 : ContentItem, new()
			where T2 : ContentItem, new()
			where T3 : ContentItem, new()
			where T4 : ContentItem, new()
		{
			return FilterContentTypes(typeof(T1), typeof(T2), typeof(T3), typeof(T4));
		}

		/// <summary>
		/// Adds a filter by type to the query. i.e. .FilterContentTypes&lt;Download, Event, Faq, Page, Program&gt;() where Download, Event, Faq, Page and Program are content types.
		/// </summary>
		/// <typeparam name="T1">The content type to filter</typeparam>
		/// <typeparam name="T2">The content type to filter</typeparam>
		/// <typeparam name="T3">The content type to filter</typeparam>
		/// <typeparam name="T4">The content type to filter</typeparam>
		/// <typeparam name="T5">The content type to filter</typeparam>
		/// <returns></returns>
		public QueryBuilder FilterContentTypes<T1, T2, T3, T4, T5>()
			where T1 : ContentItem, new()
			where T2 : ContentItem, new()
			where T3 : ContentItem, new()
			where T4 : ContentItem, new()
			where T5 : ContentItem, new()
		{
			return FilterContentTypes(typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5));
		}

		/// <summary>
		/// Adds a filter by type to the query. i.e. .FilterContentTypes(typeof(Download), typeof(Event)) where Download and Event are content types.
		/// </summary>
		/// <param name="types"></param>
		/// <returns></returns>
		public QueryBuilder FilterContentTypes(params Type[] types)
		{
			return FilterContentTypes(types.AsEnumerable());
		}

		/// <summary>
		/// Adds a filter by type to the query. i.e. .FilterContentTypes(new List&lt;Type&gt;() { typeof(Download), typeof(Event) }) where Download and Event are content types.
		/// </summary>
		/// <param name="types"></param>
		/// <returns></returns>
		public QueryBuilder FilterContentTypes(IEnumerable<Type> types)
		{
			//if (!types.Any())
			//    throw new ArgumentException("No ContentTypes specified", "types");

			foreach (var type in types)
			{
				if (type == null)
					continue;

				if (type.BaseType != typeof(ContentItem))
					throw new Exception("The type {0} does not inherit from ContentItem.".F(type.Name));

				if (type.IsAbstract)
					throw new Exception("The type {0} is abstract".F(type.Name));

				// this is not a problem
				//if (this._parameters.ContentTypes.Contains(type.Name))
				//    throw new Exception("The type {0} has been already added.".F(type.Name));

				_parameters.AddContentType(type);
			}

			return this;
		}

		/// <summary>
		/// Filters content items tagged with all the provided tag identifiers.
		/// </summary>
		/// <param name="tagIdentifiers">The tag identifiers used to filter</param>
		/// <returns></returns>
		public QueryBuilder FilterTaggedWithAll(IEnumerable<string> tagIdentifiers)
		{
			return FilterTaggedWithAll(tagIdentifiers.ToArray());
		}

		/// <summary>
		/// Filters content items tagged with all the provided tag identifiers.
		/// </summary>
		/// <param name="tagIdentifiers">The tag identifiers used to filter</param>
		/// <returns></returns>
		public QueryBuilder FilterTaggedWithAll(params string[] tagIdentifiers)
		{
			if (tagIdentifiers == null || !tagIdentifiers.Any())
				return this;

			var tags = tagIdentifiers.Where(x => x != null).Distinct();
			_parameters.TaggedWithAll.AddRange(tags);
			return this;
		}

		/// <summary>
		/// Filters content items tagged with all the provided tag identifiers.
		/// </summary>
		/// <param name="tagIdentifiers">The tag identifiers in a string separated using a separator.</param>
		/// <param name="separator">The separator char for the tag identifiers.</param>
		/// <returns></returns>
		public QueryBuilder FilterTaggedWithAll(string tagIdentifiers, char separator = ',')
		{
			return FilterTaggedWithAll(tagIdentifiers.Split(separator));
		}

        /// <summary>
        //// Filters content items tagged with all except the provided tag identifiers.
        /// </summary>
        /// <param name="tagIdentifiers">The tag identifiers in a string separated using a separator.</param>
        /// <param name="separator">The separator char for the tag identifiers.</param>
        /// <returns></returns>
        public QueryBuilder FilterNotTaggedWithAll(string tagIdentifiers, char separator = ',')
        {
            return FilterNotTaggedWithAll(tagIdentifiers.Split(separator));
        }

        /// <summary>
        /// Filters content items tagged with all except the provided tag identifiers.
        /// </summary>
        /// <param name="tagIdentifiers">The tag identifiers used to filter</param>
        /// <returns></returns>
        public QueryBuilder FilterNotTaggedWithAll(IEnumerable<string> tagIdentifiers)
        {
            return FilterNotTaggedWithAll(tagIdentifiers.ToArray());
        }

        /// <summary>
        /// Filters content items tagged with all except the provided tag identifiers.
        /// </summary>
        /// <param name="tagIdentifiers">The tag identifiers used to filter</param>
        /// <returns></returns>
        public QueryBuilder FilterNotTaggedWithAll(params string[] tagIdentifiers)
        {
            if (tagIdentifiers == null || !tagIdentifiers.Any())
                return this;

            var tags = tagIdentifiers.Where(x => x != null).Distinct();
            _parameters.NotTaggedWithAll.AddRange(tags);
            return this;
        }

		/// <summary>
		/// Filters content items tagged with any of the provided tag identifiers.
		/// </summary>
		/// <param name="tagIdentifiers">The tag identifiers used to filter</param>
		/// <returns></returns>
		public QueryBuilder FilterTaggedWithAny(IEnumerable<string> tagIdentifiers)
		{
			return FilterTaggedWithAny(tagIdentifiers.ToArray());
		}

		/// <summary>
		/// Filters content items tagged with any of the provided tag identifiers.
		/// </summary>
		/// <param name="tagIdentifiers">The tag identifiers used to filter</param>
		/// <returns></returns>
		public QueryBuilder FilterTaggedWithAny(params string[] tagIdentifiers)
		{
			if (tagIdentifiers == null || !tagIdentifiers.Any())
				return this;

			var tags = tagIdentifiers.Where(x => x != null).Distinct();
			_parameters.TaggedWithAny.AddRange(tags);
			return this;
		}

		/// <summary>
		/// Adds a filter for content items tagged with any of the provided tag identifiers.
		/// </summary>
		/// <param name="tagIdentifiers">The tag identifiers in a string separated using a separator.</param>
		/// <param name="separator">The separator char for the tag identifiers.</param>
		/// <returns></returns>
		public QueryBuilder FilterTaggedWithAny(string tagIdentifiers, char separator = ',')
		{
			return FilterTaggedWithAny(tagIdentifiers.Split(separator));
		}

		/// <summary>
		/// Filter content items featured on a specific tag identifier and feature level.
		/// </summary>
		/// <param name="tagIdentifier">The tag identifier used to filter</param>
		/// <param name="filterOperator">The operator associated with the featureLevel parameter</param>
		/// <param name="featureLevel">The feature level to filter with</param>
		/// <returns></returns>
		public QueryBuilder FilterFeaturedOnTag(string tagIdentifier, FilterOperator filterOperator, FeatureLevel featureLevel)
		{
			return FilterFeaturedOnTag(tagIdentifier, filterOperator, (int)featureLevel);
		}

		/// <summary>
		/// Filter content items featured on a specific tag identifier and feature level.
		/// </summary>
		/// <param name="tagIdentifier">The tag identifier used to filter</param>
		/// <param name="filterOperator">The operator associated with the featureLevel parameter</param>
		/// <param name="featureLevel">The feature level to filter with</param>
		/// <returns></returns>
		public QueryBuilder FilterFeaturedOnTag(string tagIdentifier, FilterOperator filterOperator, int featureLevel)
		{
			if (tagIdentifier.IsNullOrEmpty())
				return this;

			Ensure.ArgumentNotNull(filterOperator, "filterOperator");

			_parameters.FeaturedOnAllTags.Add(QueryParameters.FeaturedOnTagFilter.Create(tagIdentifier, filterOperator, featureLevel));
			return this;
		}

		/// <summary>
		/// Filter content items featured on any of the specificied tag identifiers and feature level.
		/// </summary>
		/// <param name="filterOperator">The operator associated with the featureLevel parameter</param>
		/// <param name="featureLevel">The feature level to filter with</param>
		/// <param name="tagIdentifiers">The tag identifiers used to filter</param>
		/// <returns></returns>
		public QueryBuilder FilterFeaturedOnAnyTag(FilterOperator filterOperator, int featureLevel, IEnumerable<string> tagIdentifiers)
		{
			return FilterFeaturedOnAnyTag(filterOperator, featureLevel, tagIdentifiers.ToArray());
		}

		/// <summary>
		/// Filter content items featured on any of the specificied tag identifiers and feature level.
		/// </summary>
		/// <param name="filterOperator">The operator associated with the featureLevel parameter</param>
		/// <param name="featureLevel">The feature level to filter with</param>
		/// <param name="tagIdentifiers">The tag identifiers used to filter</param>
		/// <returns></returns>
		public QueryBuilder FilterFeaturedOnAnyTag(FilterOperator filterOperator, int featureLevel, params string[] tagIdentifiers)
		{
			if (tagIdentifiers == null || tagIdentifiers.Length == 0)
				return this;

			Ensure.ArgumentNotNull(filterOperator, "filterOperator");

			foreach (var tagIdentifier in tagIdentifiers.Where(x => !x.IsNullOrEmpty()))
				_parameters.FeaturedOnAnyTags.Add(QueryParameters.FeaturedOnTagFilter.Create(tagIdentifier, filterOperator, featureLevel));

			return this;
		}

		/// <summary>
		/// Filter content items featured on a specific location and feature level.
		/// </summary>
		/// <param name="location">The location used to filter</param>
		/// <param name="filterOperator">The operator associated with the featureLevel parameter</param>
		/// <param name="featureLevel">The feature level to filter with</param>
		/// <returns></returns>
		public QueryBuilder FilterFeaturedOnLocation(string location, FilterOperator filterOperator, FeatureLevel featureLevel)
		{
			return FilterFeaturedOnLocation(location, filterOperator, (int)featureLevel);
		}

		/// <summary>
		/// Filter content items featured on a specific location and feature level.
		/// </summary>
		/// <param name="location">The location used to filter</param>
		/// <param name="filterOperator">The operator associated with the featureLevel parameter</param>
		/// <param name="featureLevel">The feature level to filter with</param>
		/// <returns></returns>
		public QueryBuilder FilterFeaturedOnLocation(string location, FilterOperator filterOperator, int featureLevel)
		{
			if (location.IsNullOrEmpty())
				return this;

			Ensure.ArgumentNotNull(filterOperator, "filterOperator");

			_parameters.FeaturedOnLocation = QueryParameters.FeaturedOnLocationFilter.Create(location, filterOperator, featureLevel);
			return this;
		}

		/// <summary>
		/// Filters content items related to the provided ContentItemId.
		/// </summary>
		/// <param name="contentItemId">The ContentItemId to be used by the relationship algorithm</param>
		/// <returns></returns>
		public QueryBuilder FilterRelatedTo(string contentItemId)
		{
			if (contentItemId.IsNullOrEmpty())
				return this;

			_parameters.RelatedToContentItemId = contentItemId;
			return this;
		}

		/// <summary>
		/// Adds a filter by List/s to the where clause
		/// </summary>
		/// <param name="listIds">The list/s IDs you want to filter by</param>
		/// <returns></returns>
		public QueryBuilder FilterList(params Guid[] listIds)
		{
			return FilterList(listIds.AsEnumerable());
		}

		/// <summary>
		/// Adds a filter by List/s to the where clause
		/// </summary>
		/// <param name="listIds">The list/s IDs you want to filter by</param>
		/// <returns></returns>
		public QueryBuilder FilterList(IEnumerable<Guid> listIds)
		{
			if (listIds == null)
				return this;

			_parameters.Lists.AddRange(listIds);
			return this;
		}

		/// <summary>
		/// Adds a filter by Site/s to the where clause
		/// </summary>
		/// <param name="siteIds">The Site/s IDs you want to filter by</param>
		/// <returns></returns>
		public QueryBuilder FilterSite(params Guid[] siteIds)
		{
			return FilterSite(siteIds.AsEnumerable());
		}

		/// <summary>
		/// Adds a filter by List/s to the where clause
		/// </summary>
		/// <param name="siteIds">The list/s IDs you want to filter by</param>
		/// <returns></returns>
		public QueryBuilder FilterSite(IEnumerable<Guid> siteIds)
		{
			if (siteIds == null)
				return this;

			_parameters.Sites.AddRange(siteIds);
			return this;
		}

		/// <summary>
		/// Adds a filter by zone/s to the where clause.
		/// </summary>
		/// <param name="zoneIds">The zone/s IDs you want to filter by</param>
		/// <returns></returns>
		public QueryBuilder FilterZone(params string[] zoneIds)
		{
			return FilterZone(zoneIds.AsEnumerable());
		}

		/// <summary>
		/// Adds a filter by zone/s to the where clause.
		/// </summary>
		/// <param name="zoneIds">The zone/s IDs you want to filter by</param>
		/// <returns></returns>
		public QueryBuilder FilterZone(IEnumerable<string> zoneIds)
		{
			if (zoneIds == null)
				return this;

			var finalZoneIds = zoneIds.Where(x => !x.IsNullOrEmpty());
			_parameters.Zones.AddRange(finalZoneIds);
			return this;
		}

		/// <summary>
		/// Order content items by the feature level associated to the provided tag identifier.
		/// </summary>
		/// <param name="tagIdentifier">The tag identifier</param>
		/// <returns></returns>
		public QueryBuilder OrderByFeaturedOnTag(string tagIdentifier)
		{
			if (tagIdentifier.IsNullOrEmpty())
				return this;

			_parameters.OrderByFeatureOnTagLevel = tagIdentifier;
			return this;
		}

		/// <summary>
		/// Order content items by the feature level associated to the provided location (FeatureLocationId).
		/// </summary>
		/// <param name="location">The location (FeatureLocationId)</param>
		/// <returns></returns>
		public QueryBuilder OrderByFeaturedOnLocation(string location)
		{
			if (location.IsNullOrEmpty())
				return this;

			_parameters.OrderByFeatureOnLocationLevel = location;
			return this;
		}

		/// <summary>
		/// Order content items randomly
		/// </summary>
		/// <returns></returns>
		public QueryBuilder OrderByRandom()
		{
			_parameters.OrderBy = "NEWID()";
			return this;
		}

		/// <summary>
		/// Orders content items using a custom SQL order by clause.
		/// </summary>
		/// <param name="orderBy">The SQL order by clause to use</param>
		/// <returns></returns>
		public QueryBuilder OrderBy(string orderBy)
		{
			_parameters.OrderBy = orderBy;
			return this;
		}

		public override string ToString()
		{
			var builder = new SqlQueryGenerator(_parameters);
			return builder.Generate();
		}

		public QueryResult<T> Get<T>() where T : ContentItem, new()
		{
			return GetInternal<T>();
		}

		public QueryResult<ContentItem> Get()
		{
			return GetInternal<ContentItem>();
		}

		public T GetFirstOrDefault<T>() where T : ContentItem, new()
		{
			return FilterContentTypes<T>().Take(1)
					   .GetInternal<T>().FirstOrDefault();
		}

		public ContentItem GetFirstOrDefault()
		{
			return Take(1)
					   .GetInternal<ContentItem>().FirstOrDefault();
		}

		public int Count()
		{
			_parameters.CountOnly = true;

			var sql = ToString();

			_parameters.CountOnly = false;

			return Context.Database.ExecuteScalar<int>(sql) ?? 0;
		}

		protected QueryResult<T> GetInternal<T>() where T : ContentItem
		{
			var queryCacheKey = "{0}_{1}".F(Constants.Cache.Query, _parameters.GetHashCode());
			if (_parameters.Cache.Behavior != QueryCacheBehavior.NoCache && _dataContext.Cache.Contains(queryCacheKey, false))
			{
				return _dataContext.Cache.Get<QueryResult<T>>(queryCacheKey);
			}

			var contentSearchResults = new QueryResult<T>(Context, _parameters);
			_dataContext.Cache.Save(queryCacheKey, contentSearchResults, TimeSpan.FromMinutes(_parameters.Cache.Duration), true);
			return contentSearchResults;
		}
	}
}